use tracing::{info, trace};

use bc_base::{
    self, BlockHeight, BlockId, PublicKeyHashBytes, TransactionId, TransactionState,
    TransactionValue,
    block::Block,
    transaction::{Transaction, TransactionOutPoint},
};
use bc_lib::transaction;

use crate::{BcManager, BcManagerError, LocalTransaction, SendTransactionRequest};

impl BcManager {
    pub async fn create_transaction(
        &self,
        _payer: &PublicKeyHashBytes,
        payee: &PublicKeyHashBytes,
        amount: TransactionValue,
    ) -> Result<Transaction, BcManagerError> {
        let wallet = self.wallet.lock().await;

        // step 1. get pub key of payer
        // let payer_pub_key = match wallet.get_pub_key(payer) {
        //     Ok(pub_key) => pub_key,
        //     Err(_) => return Err(BcManagerError::AccountNotFound(*payer).into()),
        // };

        // step 2. check balance of payer TODO:
        // if !wallet.has_enough_balance(payer, amount)? {
        //     return Err(BcManagerError::InsufficientBalance(payer.clone(), amount).into());
        // }

        // step 3. get utxos of payer
        // let utxos = wallet.get_utxos_for_spending(&payer_pub_key, amount)?;

        let fee = 1; // Simple fee for now // TODO: process fee
        let target_value = amount + fee;
        let best_height = self
            .misc_item_storage_with_cache
            .lock()
            .await
            .read_best_height();

        let coins = wallet
            .select_coins(target_value, best_height as u32)
            .await?;
        assert!(!coins.is_empty());

        // step 4. create transaction in
        let mut tx_in = Vec::new();
        let mut payer_pub_key_hash = PublicKeyHashBytes::new_null();
        for coin in coins {
            let tx = coin.get_tx();
            let tx_id = coin.calc_tx_id();
            for (index, tx_out) in tx.get_outputs().iter().enumerate() {
                let pub_key_hash = wallet.get_my_pub_key_hash_from_tx_out(tx_out)?;
                if let Some(pub_key_hash) = pub_key_hash {
                    let prev_out_point = TransactionOutPoint::new(tx_id, index as u32);
                    let payer_pub_key = wallet.get_pub_key(&pub_key_hash)?;
                    let payer_priv_key = wallet.get_private_key(&payer_pub_key)?;
                    tx_in.push(transaction::create_transaction_in(
                        &prev_out_point,
                        &payer_pub_key,
                        &payer_priv_key,
                    ));
                    payer_pub_key_hash = pub_key_hash;
                }
            }
        }

        // step 5. create transaction out
        let tx_out = vec![
            transaction::create_transaction_out(&payer_pub_key_hash, target_value - amount - fee),
            transaction::create_transaction_out(payee, amount),
        ];

        // step 6. create transaction
        let tx = transaction::create_transaction(tx_in, tx_out);

        Ok(tx)
    }

    pub async fn send_transaction(
        &self,
        request: SendTransactionRequest,
    ) -> Result<(TransactionId, TransactionState), BcManagerError> {
        // step 1. create transaction
        // let payer = &request.payer;
        let payer = PublicKeyHashBytes::new_null();
        let payee = &request.payee;
        let tx = self
            .create_transaction(&payer, payee, request.amount)
            .await?;
        let (tx_id, tx_serialize) = tx.convert_key_value();

        // step 2. process transaction
        self.process_transaction(tx_id, tx.clone(), tx_serialize)
            .await?;

        // step 3. broadcast to p2p network
        self.broadcast_tx_to_p2p_network(tx).await?;

        // step 4. send to mine task
        // self.send_tx_to_mine_task(tx.clone()).await;

        Ok((tx_id, TransactionState::Broadcasted))
    }

    pub async fn process_transaction(
        &self,
        tx_id: TransactionId,
        tx: Transaction,
        tx_serialize: Vec<u8>,
    ) -> Result<(), BcManagerError> {
        let mut consensus = self.consensus.lock().await;
        consensus
            .process_transaction(tx_id, tx, tx_serialize)
            .await?;
        Ok(())
    }

    pub async fn get_transaction(
        &self,
        tx_id: &TransactionId,
    ) -> Result<LocalTransaction, BcManagerError> {
        let consensus = self.consensus.lock().await;
        let r = consensus.get_transaction(tx_id).await?;
        Ok(r)
    }

    pub async fn process_block(&self, block: &Block) -> Result<(), BcManagerError> {
        info!("processing block");
        let mut consensus = self.consensus.lock().await;
        let accepted_blocks = consensus.process_block(block).await?;

        info!("Accepted blocks number: {}", accepted_blocks.len());

        let mut wallet = self.wallet.lock().await;
        for block in &accepted_blocks {
            wallet.add_my_txs_in_block(block).await.unwrap();
        }

        trace!("Process block end");
        Ok(())
    }

    pub async fn read_block_and_txs(&self, block_id: &BlockId) -> Result<Block, BcManagerError> {
        let consensus = self.consensus.lock().await;
        Ok(consensus.read_block_and_txs(block_id).await?)
    }
    pub async fn tx_exists_in_db(&self, tx_id: &TransactionId) -> bool {
        self.consensus.lock().await.tx_exists_in_db(tx_id).await
    }

    pub async fn read_block_height_of_tx(
        &self,
        tx_id: &TransactionId,
    ) -> Result<BlockHeight, BcManagerError> {
        let consensus = self.consensus.lock().await;
        Ok(consensus.read_block_height_of_tx(tx_id).await?)
    }

    pub async fn block_number(&self) -> usize {
        self.block_and_index_storage_with_cache
            .lock()
            .await
            .get_item_number()
            .await
    }

    pub async fn transaction_number(&self) -> usize {
        self.tx_and_index_storage_with_cache
            .lock()
            .await
            .get_item_number()
            .await
    }

    pub async fn block_exists(&self, block_id: &BlockId) -> bool {
        self.block_and_index_storage_with_cache
            .lock()
            .await
            .block_index_exists(block_id)
            .await
    }

    pub async fn transaction_exists(&self, tx_id: &TransactionId) -> bool {
        self.tx_and_index_storage_with_cache
            .lock()
            .await
            .tx_exists(tx_id)
            .await
    }

    pub async fn get_prev_block_id(&self, block_id: &BlockId) -> Option<BlockId> {
        let block_index_node = self
            .block_and_index_storage_with_cache
            .lock()
            .await
            .read_block_index_node(block_id);

        block_index_node.map(|block_index_node| *block_index_node.get_prev_block_id())
    }
}
